feat: queue persistence & recovery, queue management, streaming model/compact, collapsible panel - #335
feat: queue persistence & recovery, queue management, streaming model/compact, collapsible panel#335windli2018 wants to merge 20 commits into
Conversation
pi keeps steer/follow-up queues in memory only; a server restart silently loses them. Mirror every queue mutation to a per-session sidecar (<session>.jsonl.queue.json, atomic tmp+rename) and surface orphaned entries after a restart through a manual recovery dialog — nothing is ever auto-delivered. - lib/queue-store.ts: sidecar persistence + queue entry types - lib/rpc-manager.ts: queueMirror/queueRecovery/pendingQueueHints, queue_update reconciliation, resolve_recovery (keep/discard/continueRun via agent.continue), export_queue, import_queue commands, get_state.pendingRecovery - api/sessions/[id]/queue-recovery: lightweight sidecar read that never creates an AgentSession; live wrapper memory wins when present - QueueRecoveryDialog: checklist + re-queue / re-queue & continue / discard / export .md/.json / import / dismiss; responsive layout (desktop single row, mobile 2x2 grid + short description) - ChatInput queued banner: collapse/expand with first-message preview, export / import with inline success/error feedback, "queued" label on its own row on small screens - session DELETE removes the sidecar - next.config.ts: dev server uses its own build dir (.next-dev) so dev and production can run on separate ports simultaneously
- Queue: delete single item, reorder (drag/arrow/touch), recall-to-edit, requeue-at-position, import when empty, immediate display after import - Model: allow switching while streaming (applies next turn) with badge - Compact: queue when agent running, auto-run on idle, cancel support - Bottom panel: tri-state collapse (full/queueHidden/minimal) with swipe - Recovery: consistency check (skip sidecar restore if pi queue non-empty) - Collapsible queue summary row is now clickable to expand - i18n: add missing keys, replace hardcoded Chinese tooltips
- Importing a queue file now stages entries as pending recovery, popping the recovery dialog so the user chooses re-queue / re-queue & continue / discard (same flow as crash recovery) instead of auto-executing. - Fix "re-queue & continue" not executing: runAgentContinue captured queueMirror AFTER clearQueue (whose queue_update emptied it), so only the first entry ran. Now capture entries before clearQueue and prompt each in series — pi's steer/followUp queues aren't drained by a prompt-bootstrapped loop, so sequential prompts are the robust way to run a batch. - Recovery dialog distinguishes import vs crash-recovery copy (mode prop). - Minimal collapsed bar's "Queued · N" label is now clickable to expand. - Fix reversed swipe-hint chevron (collapse/expand arrows were swapped). - Switching back to the current run's model cancels the pending switch.
…reply (message_end)
…eing recalled entries
…d follow-up as steer
…inimal swipe-down stays minimal
…resolve/import - runAgentContinue now sends ONLY the first queued entry as a prompt and re-enqueues the rest; pi's _runAgentPrompt/agent-loop drains the followUp queue itself (agent-loop turn loop polls getFollowUpMessages), so the previous clear-all + manual per-entry prompt bootstrap is removed. - bootstrapQueuedRun: only one prompt; on failure restore the entry. - import_queue returns the mirror view so the frontend sees every imported entry even when the agent already drained pi's live arrays mid-run. - resolve_recovery: after resolving, fetch GET state to sync queuedMessages directly (not SSE-dependent) and auto-expand the queue panel. - ChatInput: expose expandQueue() on the imperative handle.
…sions too - stageImport/importQueueData: ensure the session exists first (ensureNewSession) so importing into a brand-new session works and the recovery dialog pops up instead of silently doing nothing. - ChatWindow: render QueueRecoveryDialog for new sessions as well (drop the !isNew guard) — a freshly imported queue must offer re-queue/continue.
Auto-save unsent input per session to localStorage as a base64 pseudo-cipher, keyed per tab (crashrestore-<sessionId>-<tabId>) so multi-tab edits never clobber each other and drafts survive refresh + browser close. Drafts are NOT auto-restored: on reopen every surviving slot for the session is loaded and listed as one item in the queue-recovery dialog (DRAFT badge). Keep restores the first draft into the input box and queues the rest as follow-ups; discard wipes the slot; storage events sync the dialog across tabs. Session deletion clears all slots. - lib/draft-store.ts: per-tab slots, debounced writes + unload flush, listDraftSlots/removeDraftSlot/clearAllDrafts - ChatWindow: pendingDrafts + recovery-item composition + client-side resolution - ChatInput: no auto-restore (empty box never wipes a stored draft), restoreDraft/hasInput handle methods - QueueRecoveryDialog: draft badge, hide timestamp for drafts - AppShell: clearAllDrafts on session delete
- QueuedMessageRow: add user-select:none (+ -webkit-user-select, -webkit-touch-callout) so a long-press on small screens starts the touch-drag instead of the browser's native text selection (touchAction alone does not prevent it). - Queue panel: default to expanded and never auto-hide by count — the queue is hidden only when the user explicitly collapses it. Moving / removing / dropping a queued message no longer resets the user's manual collapse choice (auto-hide made the panel disappear right after editing).
…queue touch-drag - chat.queueSteerSend was used by the queue row's steer-send button but missing from en/zh-CN on this branch (added later upstream) — restore it. - handleTouchMove called e.preventDefault() on a touchmove listener React registers as passive: it warned and was a no-op. Scrolling is already prevented by touch-action:none on the row, so the call is just removed.
|
Thanks for the PR, but I think we should not merge this for now. The queue is primarily short-lived runtime input. Multiple messages intended for the same delivery point can usually be combined into one, and the existing in-memory queue is sufficient for that use case. Unless we have clear user feedback or evidence that losing undelivered messages on restart is a frequent problem, I do not think the persistence sidecar, recovery flow, and additional state management are justified. This PR also bundles several unrelated changes, including queue management, import/export, model switching and compaction while running, and mobile UI changes. The scope is too broad. If a real persistence need emerges, please propose it later as a separate, minimal PR. |
What
pi keeps steer/follow-up queues in memory only — a server restart silently loses them. This PR mirrors every queue mutation to a per-session sidecar (
<session>.jsonl.queue.json) and surfaces orphaned entries after a restart through a manual recovery dialog. Nothing is ever auto-delivered: the user decides per-entry to re-queue, re-queue & continue, discard, or export.On top of crash recovery, this PR adds a full set of queue management operations (delete/reorder/recall/requeue), imports that pop the same recovery dialog so the user chooses how to run an imported queue, lets users switch models and compact while the agent is running, and ships a collapsible bottom panel for small screens.
How — crash recovery
lib/queue-store.ts(new): atomic sidecar persistence (tmp+rename) + entry types.lib/rpc-manager.ts:queueMirror(live queue w/ images) /queueRecovery(orphans) /pendingQueueHints(FIFO image hints — pi reports queued items by text only and template expansion can rewrite text).queue_updateevents reconcile the mirror. New commands:resolve_recovery {keep, discard, continueRun}— re-queues viainner.steer()/followUp().continueRunruns the whole batch: pi has no public API to resume an idle agent's queue (agent.continue()only works inside an active_runAgentPromptloop, and the internal continuation stalls), sobootstrapQueuedRunprompts each kept entry in series — eachAgentSession.prompt()awaits its_runAgentPrompt, so entries run one turn after another. (Entries are captured beforeclearQueue, whosequeue_updatewould otherwise empty the mirror.)stage_recovery {entries}— stage entries as pending recovery (NOT into the live queue) so the recovery dialog pops up; used by import.export_queue,import_queueget_state.pendingRecoveryapi/sessions/[id]/queue-recovery(new): lightweight sidecar read that never creates an AgentSession; when a wrapper is alive its in-memory recovery list wins.QueueRecoveryDialog(new): checklist with re-queue / re-queue & continue / discard / export.md/.json/ import / dismiss. Amodeprop ("recovery"|"import") swaps the title/description so an import shows import copy instead of crash-recovery copy. Responsive: desktop single-row footer, mobile 2×2 grid + shorter description; matches the project's existing modal conventions (fixed overlay,var(--bg), radius 10).How — queue operations
pi has no single-dequeue / reorder / insert API, so all edits are implemented as clear-all + re-enqueue in the target order (image hints preserved FIFO), mirroring pi's own
clearQueue()semantics (allowed mid-run since the loop only pulls entries between turns). New commands:move_queue {kind, fromIndex, toIndex}— reorder within a queue.recall_queue_item {kind, index}— pull one entry back to the input box for editing (returns its text + images).requeue_at {kind, index, text, images}— insert an edited message back at its original position. Sending from the input while arecalledRefis active re-queues instead of prompting.remove_queue_item {kind, index}— delete a single entry.import_queuereturns the queue snapshot (steering/followUp) so the UI updates immediately without a reload.UI: each queued row has ↑/↓ (reorder), ↩ (recall to edit), 🗑 (delete), plus HTML5 drag on desktop and touch drag on mobile (
pointer: coarseauto-disables native draggable). The queue banner collapses to a first-message preview (… +N) which is clickable to expand; in the minimal collapsed state the "Queued · N" label is also clickable to expand. Export/import buttons are always reachable (incl. when the queue is empty). Importing a file stages entries viastage_recoveryand pops the recovery dialog — the user then picks re-queue / re-queue & continue / discard, rather than the queue auto-executing.How — model switch & compact while running
setModel()only overwritesagent.state.model, so the in-flight request still finishes with the old model and the next turn picks up the new one — i.e. it already "applies next turn". This PR simply removes the UIdisabled={isStreaming}and adds an amber ⏱ "next turn" badge on the model button that auto-clears when the run ends. Switching back to the model the current run started with cancels the pending switch (badge clears) — the run model is captured on theagentRunningtransition and intentionally not overwritten by mid-run switches.compact()rewrites context + the session file, so running it mid-loop is unsafe.handleCompactqueues apendingCompactwhenagentRunningis true (covers streaming and tool-call execution, whereisStreamingis briefly false), and an effect drains it once the agent is truly idle (prompt_donealone is not enough — retries / auto-compaction / queued follow-ups can extend a run). The button shows a three-state cycle: idle → amber "Queued" (click to cancel) → red "Compacting".How — small-screen bottom panel
ChatInput gains a tri-state
bottomMode(full→queueHidden→minimal, persisted inlocalStorage): a Windows-10-style vertical bar button in the toolbar, plus swipe gestures (swipe down to collapse one step, up to expand) with a hint overlay (chevron points in the swipe direction). Queue-row touch events are isolated (stopPropagation) so dragging to reorder never triggers mode switching.How — recovery safety
When a wrapper is (re)created with a non-empty pi queue, sidecar restore is skipped and the mirror is rebuilt from the live queue — preventing double-enqueue if the architecture ever changes so pi-web and pi can restart independently.
Why not auto-restore?
Auto-replaying queued messages after a restart risks double-delivery (messages already delivered but still mirrored) and stale-queue resurrection. Manual recovery keeps it explicit. The same reasoning applies to imports: staging them through the dialog lets the user decide whether to run, rather than auto-executing.
Notes
_steeringMessages(falsycontentText("")) — they may surface as already-delivered in recovery; users discard manually (documented in AGENTS.md).next.config.ts: dev server gets its own build dir (.next-dev) so dev and production can run on separate ports simultaneously.tsconfig.json/.gitignoreupdated for it.